--[[
Things the library doesn't handle well:
1.) Sometimes there are combat log entries like this: -6000 (damage), +5000 (heal). But what really happened on the server was that the heal occured first. If at least part of this heal was overheal, the prediction (-6000+5000 = -1000) is way off, because what happened is for example +50 heal first(4950 overheal), and then 6000 damage, for a total of +50-6000 = -5950. No AddOn could possibly prevent or predict this, but luckily these cases are very rare. 
However, if you see something like this happening (tank at ~100%, then takes damage instantly followed by a heal), you should know that this heal might have been overheal. So don't cancel your heal. ;)

2.) If the maximum health changes, pending damage is completely ignored. Example: The tank is at 11001/18000, takes 9001 damage and uses Last Stand at the same time. Your client might tell you now that the tank health is 16401/23400 (+30% health and max health), because that 9k hit was not yet processed (pending damage). What the library does is: Show the tank at 11001/18000, then at 11001-9001 = 2000/18000, then at 16401/23400, because pending damage is ignored. But on the server, the tank has only 16401-9001 = 7400/23400 health!
This looks very similar to the case before: The tank takes damage, but shortly afterwards that damage is (magically) undone. Again, if you see this, don't cancel your heal!

3.) Bloodrush. There's no combat log entry for the health lost. I believe the health cost depends on the caster level and race, but I don't know the exact formula.

4.) Exceptionally large HP5 values. There are no combat log entries for HP5 ticks, which occur every 2 seconds. The library assumes that the health regen in combat is between 0 and 50 HP5, i.e. HP5 ticks between 0 and 20. 
Common HP5 values: Demon skin/armor <=18 (24 talented?), Elixir of Major Fortitude 10, Enchant Boots - Vitality 4. Less common: Dreaming Glory 30 (buff from herbalism), and a few level 60 items. 
So the library might get confused by 2 things: Small amounts of pending damage (20 or less), and Warlocks with herbalism...

--]]

------------------------------------------------------------------------------------------------------------
-- Library part
------------------------------------------------------------------------------------------------------------
do -- library

local MAJOR, MINOR = "LibQuickHealth-2.0", 1
local QuickHealth, oldminor = LibStub:NewLibrary(MAJOR, MINOR)

QuickHealth.events = QuickHealth.events or LibStub("CallbackHandler-1.0"):New(QuickHealth);

local healthFromGUID = {}
local healthMaxFromGUID = {}
local healthQueueFromGUID = {}

QuickHealth.eventHandlers = QuickHealth.eventHandlers or {}
local eventHandlers = QuickHealth.eventHandlers
local eventFrame
local initialized

-- API
function QuickHealth:UnitHealth(unitID)
	local guid = UnitGUID(unitID)
	return guid and healthFromGUID[guid] or UnitHealth(unitID)
end

function QuickHealth:UnitHealthQueue(unitID)
	local guid = unitID
	if guid and not healthQueueFromGUID[guid] then
		guid = UnitGUID(unitID)
	end
	if guid then
		return unpack(healthQueueFromGUID[guid])
	end
end


if(QuickHealth.eventFrame) then
	eventFrame = QuickHealth.eventFrame
else
	eventFrame = CreateFrame("Frame")
	QuickHealth.eventFrame = eventFrame
	eventFrame:SetScript("OnEvent", function(self, event, ...) eventHandlers[event](...) end)
end

local HealthUpdatedRegistered
local HealthUpdatedDebugRegistered

local GetFrameNumber
do
	local counter = 0
	function GetFrameNumber() 
		return counter 
	end
	local function IncrementCounter()
		counter = counter+1 
	end

	-- When an addon starts to listen for HealthUpdated we register our events and actualy start doing something
	function QuickHealth.events:OnUsed(target, eventName)
		if eventName == "HealthUpdatedDebug" and not HealthUpdatedDebugRegistered then
			counter = 0
			eventFrame:SetScript("OnUpdate", IncrementCounter)
		end

		local register = eventName == "HealthUpdated" or eventName == "HealthUpdatedDebug"
		if register and not (HealthUpdatedRegistered or HealthUpdatedDebugRegistered) then
			eventFrame:RegisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
			eventFrame:RegisterEvent("UNIT_MAXHEALTH");
			eventFrame:RegisterEvent("UNIT_HEALTH");
			eventFrame:RegisterEvent("PLAYER_UNGHOST");
		end
		if eventName == "HealthUpdated" then
			HealthUpdatedRegistered = true
		end
		if eventName == "HealthUpdatedDebug" then
			HealthUpdatedDebugRegistered = true
		end

		if not initialized then
			if UnitHealth('player') and eventHandlers.PLAYER_LOGIN then
				eventHandlers.PLAYER_LOGIN()
			else
				eventFrame:RegisterEvent("PLAYER_LOGIN")
			end
		end
	end

	-- Unregister the events and empty the healthtable
	function QuickHealth.events:OnUnused(target, eventName)
		if eventName == "HealthUpdated" then
			HealthUpdatedRegistered = nil
		end
		if eventName == "HealthUpdatedDebug" then
			HealthUpdatedDebugRegistered = nil
			eventFrame:SetScript("OnUpdate", nil)
		end
		if not (HealthUpdatedRegistered or HealthUpdatedDebugRegistered) then
			eventFrame:UnregisterEvent("COMBAT_LOG_EVENT_UNFILTERED");
			eventFrame:UnregisterEvent("UNIT_MAXHEALTH");
			eventFrame:UnregisterEvent("UNIT_HEALTH");
			eventFrame:UnregisterEvent("PLAYER_UNGHOST");
		end
	end
end

local function clear(queue)
	for i=#queue,1,-1 do
		queue[i]=nil
	end
end

local MAX_QUEUE_SIZE = 5
local MAX_HP5_TICK = 20 -- 50 HP5
local function processUH(guid, unitHealth, unitHealthMax)
	if unitHealthMax == 100 then
		-- We might get here for unitID == target
		healthFromGUID[guid] = nil
		healthMaxFromGUID[guid] = nil
		healthQueueFromGUID[guid] = nil
		return
	end

	local health = healthFromGUID[guid]
	if not health then
		healthFromGUID[guid] = unitHealth
		healthMaxFromGUID[guid] = unitHealthMax
		healthQueueFromGUID[guid] = {0,0,0,0,0}
		clear(healthQueueFromGUID)
		if HealthUpdatedRegistered then
			QuickHealth.events:Fire("HealthUpdated", guid, unitHealth, unitHealthMax)
		end
		return
	end

	local healthMax = healthMaxFromGUID[guid]
	local healthQueue = healthQueueFromGUID[guid]
	local fire
	if healthMax ~= unitHealthMax then
		-- TODO If healthMax changes, one of three things can happen:
		-- 1. health doesn't change (except if health>unitHealthMax ofc)
		-- 2. health changes by unitHealthMax-healthMax
		-- 3. health/healthMax doesn't change: h1/m1 = h2/m2, h2 = h1*m2/m1, h2-h1 = h1*(m2/m1-1)
		healthMax = unitHealthMax
		healthMaxFromGUID[guid] = unitHealthMax
		-- For now, just reset and hope it's correct. 
		health = unitHealth
		healthFromGUID[guid] = unitHealth
		fire = true
	end

	-- Ex unitHealth = 104, health = 100
	local discrepancy = unitHealth - health
	if discrepancy == 0 then
		clear(healthQueue)
	elseif #healthQueue==0 then
		health = unitHealth
		healthFromGUID[guid] = unitHealth
		fire = true
	elseif 0<discrepancy and discrepancy<=MAX_HP5_TICK then
		clear(healthQueue)
		health = unitHealth
		healthFromGUID[guid] = health
		fire = true
	else
		local noexplanation = true

		local numdamage = 0
		local pendingDamage
		local healthChanged
		do
			-- Case 1: The last damage entries are pending.
			-- Ex. health = 1000-200-100+250 = 950, healthMax = 1000, unitHealth = 1000, 
			-- queue = (-200, -100, +250) = (-200, +200, -100) -> (-100)
			local h=health
			local pending = 0
			local overheal = 0
			for i=#healthQueue, 1, -1 do
				local amount = healthQueue[i]
				if amount<0 then
					numdamage = numdamage+1
					-- We assume that amount is pending, so it didn't count yet.
					-- Ex. i=2: h = 1000-(-100) = 1100
					h = h - amount
					pending = pending + amount
					-- Check for possible OHs.
					if h > healthMax then
						-- Ex. i=2: h=1000
						overheal = overheal + h-healthMax
						h = healthMax
					end
					local diff = unitHealth - h
					if 0<=diff and diff<=MAX_HP5_TICK then
						noexplanation = nil
						if diff>0 or overheal>0 then
							-- If we get here, our explanation is that an HP5 tick occured.
							healthChanged = true
						end
						pendingDamage = pending
						break
					end
				end
			end
		end

		if noexplanation and numdamage>1 then
			-- Case 2: A single damage entry is pending, but not the last one.
			for i=#healthQueue, 1, -1 do
				local amount = healthQueue[i]
				if amount<0 then
					-- See what happens if amount is pending.
					local h = health - amount
					-- Check for OH.
					if h>healthMax then
						h = healthMax
					end
					local diff = unitHealth - h
					if 0<=diff and diff<=MAX_HP5_TICK then
						noexplanation = nil
						if diff>0 then
							-- If we get here, our explanation is that an HP5 tick occured.
							healthChanged = true
						end
						pendingDamage = amount
						break
					end
				end
			end
		end

		if pendingDamage then
			-- Remove everything except the pending damage.
			healthQueue[1] = pendingDamage
			for j=#healthQueue, 2, -1 do
				healthQueue[j]=nil
			end
			if healthChanged then
				health = unitHealth + pendingDamage
				healthFromGUID[guid] = health
				fire = true
			end
		end

		if noexplanation then
			-- Case 3: Nothing's pending, but the order of the combat log entries might have been wrong.
			-- If this results in more overheal than predicted, our prediction was wrong.
			-- We can't do anything about that. Just reset.
			-- Examples: 
			-- 19,179: (-a,+b,-c,+d) = (+b OH, -a, -c, +d) -> ()
			-- 19,1169: (-a,+b) = (+b OH,-a) -> ()
			-- 19,1426: (-a,-b,+c) = (-a,+c OH,-b) -> ()
			-- 19,1253: (-a pending, +b) = (+b OH, -a) -> ()
			-- 19,1942: (-a pending, +b,+c) = (+b,+c OH, -a) -> ()   [can't be (+b OH,-a,+c)]
			clear(healthQueue)
			health = unitHealth
			healthFromGUID[guid] = unitHealth
			fire = true
		end
	end

	if --[[fire and]] HealthUpdatedRegistered then
		QuickHealth.events:Fire("HealthUpdated", guid, health, healthMax)
	end
end

-- Amount is just the raw value from the combat log, including overheal.
local function processCLEU(guid, amount, amountMax)
	local health = healthFromGUID[guid]
	if not health then return end
	local healthQueue = healthQueueFromGUID[guid]
	local healthMax = healthMaxFromGUID[guid]

	if amountMax then
		healthMax = healthMax+amountMax
		healthMaxFromGUID[guid] = healthMax
	end

	local healthDiff = amount
	if health+amount>healthMax then
		healthDiff = healthMax-health
	end
	--print(healthDiff)

	if healthDiff~=0 then
		tinsert(healthQueue, healthDiff)
		if #healthQueue>MAX_QUEUE_SIZE then
			local first = table.remove(healthQueue, 1)
			healthQueue[1] = healthQueue[1]+first
		end
		health = health+healthDiff
		healthFromGUID[guid] = health
		if HealthUpdatedRegistered then
			QuickHealth.events:Fire("HealthUpdated", guid, health, healthMax)
		end
	end
end

eventHandlers.CLEU = processCLEU
eventHandlers.UH = processUH


function eventHandlers.PLAYER_LOGIN()
	for _,unitID in pairs({"player","pet"}) do
		if UnitHealth(unitID) and UnitGUID(unitID) then
			initialized = true
			eventHandlers.UNIT_HEALTH(unitID)
		end
	end
	if initialized then
		eventHandlers.PLAYER_LOGIN = nil
	end
end

function eventHandlers.PLAYER_UNGHOST()
	eventHandlers.UNIT_HEALTH("player")
end

function eventHandlers.UNIT_HEALTH(unitID)
	if unitID == 'target' or unitID == 'focus' or unitID == 'mouseover' then return end
	local unitHealth = UnitHealth(unitID)
	local unitHealthMax = UnitHealthMax(unitID)

	if HealthUpdatedDebugRegistered then
		QuickHealth.events:Fire("HealthUpdatedDebug", UnitGUID(unitID), 1, GetTime(), GetFrameNumber(), unitHealth, unitHealthMax)
	end

	processUH(UnitGUID(unitID), unitHealth, unitHealthMax)
end

function eventHandlers.UNIT_MAXHEALTH(unitID)
	if unitID == 'target' or unitID == 'focus' or unitID == 'mouseover' then return end
	local unitHealth = UnitHealth(unitID)
	local unitHealthMax = UnitHealthMax(unitID)
	if HealthUpdatedDebugRegistered then
		QuickHealth.events:Fire("HealthUpdatedDebug", UnitGUID(unitID), 2, GetTime(), GetFrameNumber(), unitHealth, unitHealthMax)
	end

	processUH(UnitGUID(unitID), unitHealth, unitHealthMax)
end

--[[
Spells that affect HP and/or max HP.

Bloodrage (Warrior)
id 2687 
cost 16% of base health, whatever that is (711 HP Human 70)
8/31 15:13:54.109  SPELL_CAST_SUCCESS,0x0000000000540733,"Tifi",0x40511,0x0000000000000000,nil,0x80000000,2687,"Blutrausch",0x1
8/31 15:13:54.109  SPELL_ENERGIZE,0x0000000000540733,"Tifi",0x40511,0x0000000000540733,"Tifi",0x40511,2687,"Blutrausch",0x1,10,1
8/31 15:13:54.421  SPELL_AURA_APPLIED,0x0000000000000000,nil,0x80000000,0x0000000000540733,"Tifi",0x40511,29131,"Blutrausch",0x1,BUFF
8/31 15:13:54.968  SPELL_PERIODIC_ENERGIZE,0x0000000000540733,"Tifi",0x40511,0x0000000000540733,"Tifi",0x40511,29131,"Blutrausch",0x1,1,1
...
8/31 15:14:03.984  SPELL_PERIODIC_ENERGIZE,0x0000000000540733,"Tifi",0x40511,0x0000000000540733,"Tifi",0x40511,29131,"Blutrausch",0x1,1,1
8/31 15:14:04.000  SPELL_AURA_REMOVED,0x0000000000000000,nil,0x80000000,0x0000000000540733,"Tifi",0x40511,29131,"Blutrausch",0x1,BUFF

Commanding Shout
id 469
max+=1080 
1350 talented (+25%, 54 per talent point)
T6 2 set bonus: Increases the health bonus from your Commanding Shout ability by 170.
Commanding Shout maintains your current health percentage when it is used or goes away.
8/31 15:15:04.890  SPELL_CAST_SUCCESS,0x0000000000540733,"Tifi",0x40511,0x0000000000000000,nil,0x80000000,469,"Befehlsruf",0x1
8/31 15:15:05.187  SPELL_AURA_APPLIED,0x0000000000000000,nil,0x80000000,0x0000000000540733,"Tifi",0x40511,469,"Befehlsruf",0x1,BUFF
8/31 15:15:05.984  SPELL_AURA_APPLIED,0x0000000000000000,nil,0x80000000,0x00000000002F4F84,"Pelztier",0x512,469,"Befehlsruf",0x1,BUFF
8/31 15:15:05.984  SPELL_AURA_APPLIED,0x0000000000000000,nil,0x80000000,0x0000000000ABFC84,"Creativ",0x512,469,"Befehlsruf",0x1,BUFF
8/31 15:15:05.984  SPELL_AURA_APPLIED,0x0000000000000000,nil,0x80000000,0x0000000000B71120,"Talho",0x512,469,"Befehlsruf",0x1,BUFF
8/31 15:15:05.984  SPELL_AURA_APPLIED,0x0000000000000000,nil,0x80000000,0x0000000000642235,"Styxe",0x512,469,"Befehlsruf",0x1,BUFF

Last Stand (Warrior)
id 12975
gain hp+=30% max, max*=130%
loss max/=130%
On consideration, it's a little odd that someone can make their "LAST. STAND." every eight minutes, but I guess warriors are just really, really emotional people.
8/31 15:30:32.515  SPELL_CAST_SUCCESS,0x0000000000540733,"Tifi",0x40511,0x0000000000000000,nil,0x80000000,12975,"Letztes Gefecht",0x1
8/31 15:30:32.703  SPELL_AURA_APPLIED,0x0000000000000000,nil,0x80000000,0x0000000000540733,"Tifi",0x40511,12976,"Letztes Gefecht",0x1,BUFF
8/31 15:30:52.812  SPELL_AURA_REMOVED,0x0000000000000000,nil,0x80000000,0x0000000000540733,"Tifi",0x40511,12976,"Letztes Gefecht",0x1,BUFF

Nightmare Seed
id 28726
hp+max 2000

Trinkets with similar effects:
Audacity http://www.wowhead.com/?item=34049 - bought for 75 Badges of justice.
Cruelty http://www.wowhead.com/?item=34163 - Bought for 75 badges of justice.
Depravity http://www.wowhead.com/?item=34162 - Bought for 75 badges of justice.
Determination http://www.wowhead.com/?item=33832 - Bought for 75 badges of justice.
Lifegiving Gem http://www.wowhead.com/?item=19341- Drop in Blackwing Lair.
Perseverance http://www.wowhead.com/?item=34050 - Bought for 75 badges of justice.
Shadowmoon insignia http://www.wowhead.com/?item=32501 - Dropped in Black Temple.
Brooch of the immortal king http://www.wowhead.com/?item=32534 - Dropped in Terokkar Forest.
Regal Protectorate http://www.wowhead.com/?item=28042 - From quest in Hellfire Peninsula.



--]]

-- Life Tap
-- Don't compute the exact cost (which depends on the caster's level), just the max cost. 
-- The maximum error is 10, and errors only occur if caster level < 70, so it's no big deal.
-- CLEUs:
-- 8/8 19:55:40.796  SPELL_CAST_SUCCESS,0x0000000000A6DA0B,"Devora",0x511,0x0000000000000000,nil,0x80000000,1454,"Aderlass",0x20
-- 8/8 19:55:40.812  SPELL_ENERGIZE,0x0000000000A6DA0B,"Devora",0x511,0x0000000000A6DA0B,"Devora",0x511,31818,"Aderlass",0x20,24,0
-- Ignore T3 8/8 set bonus: Reduces health cost of your Life Tap by 12%.
local lifeTapHealthMax = {}
do
	local lifeTapHealth = {
		[1454] = -20,-- rank 1
		[1455] = -60,-- rank 2
		[1456] = -130,-- rank 3
		[11687] = -210,-- rank 4
		[11688] = -300,-- rank 5
		[11689] = -420,-- rank 6
		[27222] = -580,-- rank 7
	}
	local lifeTapLevel = {
		[1454] = 6,-- rank 1
		[1455] = 16,-- rank 2
		[1456] = 26,-- rank 3
		[11687] = 36,-- rank 4
		[11688] = 46,-- rank 5
		[11689] = 56,-- rank 6
		[27222] = 68,-- rank 7
	}
	for spellID,baseCost in pairs(lifeTapHealth) do
		-- maxLevel for rank 1 is 16, rank 7 is 70.
		local maxLevel = math.min(lifeTapLevel[spellID]+10,70)
		-- maxBonus for rank 1 is 10, rank 7 is 2.
		local maxBonus = maxLevel - lifeTapLevel[spellID]
		-- rank 1 -30, rank 7 -582.
		lifeTapHealthMax[spellID] = baseCost - maxBonus
	end
end

local flagRaid = bit.bor(COMBATLOG_OBJECT_AFFILIATION_MINE, COMBATLOG_OBJECT_AFFILIATION_PARTY, COMBATLOG_OBJECT_AFFILIATION_RAID)
local flagPlayerPets = bit.bor(COMBATLOG_OBJECT_TYPE_PLAYER, COMBATLOG_OBJECT_TYPE_PET)
local bitband = bit.band
function eventHandlers.COMBAT_LOG_EVENT_UNFILTERED(timestamp, event, sourceGUID, sourceName, sourceFlags, destGUID, destName, destFlags, ...)
	local amount
	local amountMax
	local guid
	
	--local playerGUID = UnitGUID('player')
	--if destGUID == playerGUID then
	if bitband(destFlags, flagRaid)>0 and bitband(destFlags, flagPlayerPets)>0 then
		-- We only care about damage or healing, check for either of those and react accordingly
		if(event == "SWING_DAMAGE") then --autoattack dmg
			amount = -(...) -- putting in braces will autoselect the first arg, no need to use select(1, ...)
		elseif(event == "SPELL_PERIODIC_DAMAGE" or event == "SPELL_DAMAGE"
			or event == "DAMAGE_SPLIT" or event == "DAMAGE_SHIELD") then -- all kinds of spelldamage
			amount = -select(4, ...)
		elseif(event == "SPELL_HEAL" or event == "SPELL_PERIODIC_HEAL") then --healing
			amount = select(4, ...)
		elseif(event == "ENVIRONMENTAL_DAMAGE") then --environmental damage
			amount = -select(2, ...)
--[[
		else
			local applied = event == "SPELL_AURA_APPLIED"
			if applied or event == "SPELL_AURA_REMOVED" then
				local spellID = (...)
				if spellID == 469 then
					-- TODO Commanding Shout
					local health = healthFromGUID[destGUID]
					if health then
						local healthMax = healthMaxFromGUID[destGUID]
						local p = health/healthMax
						if applied then
							amount = math.floor((healthMax+1080)*p + 0.5)
							amountMax = 1080
						else
							amount = math.floor((healthMax-1080)*p + 0.5)
							amountMax = -1080
						end
					end
				end
			end
--]]
		end
		guid = destGUID
	-- elseif sourceGUID == playerGUID then
	elseif bitband(sourceFlags, flagRaid)>0 and bitband(sourceFlags, flagPlayerPets)>0 then
		if event == "SPELL_CAST_SUCCESS" then
			local spellID = (...)
			-- TODO Check only for locks?
			local lifeTapMax = lifeTapHealthMax[spellID]
			if lifeTapMax then
				amount = lifeTapMax
--[[
			elseif spellID==2687 then
				-- TODO Bloodrage
				amount = -711
--]]
			elseif spellID==12975 then
				-- Last Stand; this should be pretty exact because healthMax doesn't change
				local healthMax = healthMaxFromGUID[sourceGUID]
				if healthMax then
					amount = math.floor(0.3 * healthMax + 0.5)
					amountMax = amount
				end
			end
		end
		guid = sourceGUID
	end

	if amount then
		if HealthUpdatedDebugRegistered then
			QuickHealth.events:Fire("HealthUpdatedDebug", guid, 3, GetTime(), GetFrameNumber(), amount, amountMax)
		end
		processCLEU(guid, amount, amountMax)
	end
end


end -- library implementation




------------------------------------------------------------------------------------------------------------
-- Test Addon
------------------------------------------------------------------------------------------------------------
local f = format
local channelIndex
local chatFrame
local currentUnit
local QuickHealth = LibStub("LibQuickHealth-2.0")
setglobal("lqh", QuickHealth)

qhtEventLog = {}
local function p(s,...)
	if not chatFrame then
		for i=1,8 do
			local tab = getglobal('ChatFrame'..i..'Tab')
			if tab and tab.GetText and tab:GetText()=='QuickHealthTest' then
				chatFrame = getglobal('ChatFrame'..i)
			end
		end
	end
	local msg = f(s,...)
	if LoggingChat() and channelIndex then
		SendChatMessage(msg, "CHANNEL", nil, channelIndex)
	end
	if chatFrame then
		chatFrame:AddMessage(msg)
	end
end

local function t()
	local now = GetTime()
	return math.fmod(now, 10)
end

-- Replay recording.
do
	local recordedUnit
	local eventLog
	local CURRENT_REPLAY_FORMAT = 1
	local function logfunc(event, guid, ...)
		if guid==UnitGUID(recordedUnit) then
			tinsert(eventLog, {guid, ...})
		end
	end
	function qhtLogging(unitID)
		if unitID and type(unitID) ~="string" then
			p('Syntax: qhtLogging("unitID" or nil)')
			return
		end
		if unitID == recordedUnit then
			return recordedUnit
		end
		if unitID then
			-- Make sure we're not replaying
			qhtstop()
			-- Create new replay.
			eventLog = {}
			tinsert(qhtEventLog, {
				unitID = unitID,
				replayformat = CURRENT_REPLAY_FORMAT,
				events = eventLog,
			})
			p('Recording %s', unitID)
			if not recordedUnit then
				QuickHealth.RegisterCallback("QuickHealthTestLog", "HealthUpdatedDebug", logfunc)
			end
		else
			QuickHealth.UnregisterCallback("QuickHealthTestLog", "HealthUpdatedDebug")
			p('Recording disabled')
		end
		recordedUnit = unitID
		return recordedUnit
	end
end

-- Replay playback.
do
	local replaying
	local timediff
	local index
	local nexttime
	local starttime
	local OnUpdate
	local log

   local h = QuickHealth.eventHandlers
   local f={h.UH, h.UH, h.CLEU}

	local OnUpdates = {}
	local previousUnit

	function qhtreplay(i, startindex)
		-- Make sure we're not logging.
		qhtLogging()
		if replaying then
			qhtstop()
		end
		if not qhtEventLog[i] then 
			p("Log #%i doesn't exist.",i)
			return 
		end
		local format = qhtEventLog[i].replayformat
		if not format then
			log = qhtEventLog[i].player
		else
			log = qhtEventLog[i].events
		end
		local unitID = qhtEventLog[i].unitID or "player"
		if #log==0 then 
			p("Log #%i is empty.",i)
			return
		end
		local now = GetTime()
		
		index = 1
		if startindex and #log>=startindex then
			index = startindex
		end
		-- If the first startindex we're looking at is a CLEU, the display would be out of sync.
		if not format then
			local id, time
			while log[index] do
				id, time = unpack(log[index])
				if id==1 or id==2 then
					break
				end
				index = index+1
			end
			if not log[index] then
				p("Log #%i contains no UNIT_HEALTH events after entry %i.", i, startIndex or 1)
				return
			end
			-- Ex. We start replaying at 400.
			-- The first log startindex is at 41000.
			-- timediff = 40599
			-- nexttime = 41000
			-- nexttime-timediff = 401
			nexttime = time
		else
			local guid, id, time
			while log[index] do
				guid, id, time = unpack(log[index])
				if id==1 or id==2 then
					break
				end
				index = index+1
			end
			nexttime = time
		end
		timediff = nexttime - (now + 1)

		p("Replaying log #%i, unit = %s, %i entries", i, unitID, #log)
		--p("now=%f, nexttime=%f, timediff=%f, nexttime-timediff=%f", now + 1, nexttime, timediff, nexttime-timediff)

		replaying = true
		if currentUnit~="replay" then
			previousUnit = currentUnit
		end
		currentUnit = "replay"
		if not format then
			qhtFrame:SetScript("OnUpdate",OnUpdate)
		else
			qhtFrame:SetScript("OnUpdate",OnUpdates[format])
		end
	end

	function qhtstop()
		p("Stopping replay.")
		if replaying then
			qhtFrame:SetScript("OnUpdate",nil)
			qhtSetCurrentUnit(previousUnit)
			previousUnit = nil
			replaying = nil
		end
	end

	local MAX_WAIT = 2.1
	function OnUpdate()
		local now = GetTime()
		if now >= nexttime-timediff then
			local id,time,frame,hp,max = unpack(log[index])
			-- TODO
			qhtHealthUpdatedDebug(index, "replay", id,time,frame,hp,max)
			f[id](UnitGUID("player"), hp, max)
			if id==1 then
				qhtUNIT_HEALTH(hp,max)
			end

			index = index+1
			if log[index] then
				nexttime = log[index][2]
				if not (now + MAX_WAIT >= nexttime-timediff) then
					timediff = nexttime - (now + MAX_WAIT)
				end
				--p("now=%f, nexttime=%f, timediff=%f, nexttime-timediff=%f", now + 1, nexttime, timediff, nexttime-timediff)
			else
				qhtstop()
				p("Replay finished.")
			end
		end
	end

	-- Replay format 1.
	OnUpdates[1] = function()
		local now = GetTime()
		if now >= nexttime-timediff then
			local guid, id,time,frame,hp,max = unpack(log[index])
			qhtHealthUpdatedDebug(index, guid, id,time,frame,hp,max)
			f[id](UnitGUID("player"), hp, max)
			if id==1 then
				qhtUNIT_HEALTH(hp,max)
			end

			index = index+1
			if log[index] then
				nexttime = log[index][3]
				if not (now + MAX_WAIT >= nexttime-timediff) then
					timediff = nexttime - (now + MAX_WAIT)
				end
				--p("now=%f, nexttime=%f, timediff=%f, nexttime-timediff=%f", now + 1, nexttime, timediff, nexttime-timediff)
			else
				qhtstop()
				p("Replay finished.")
			end
		end
	end
end

function qhtlogs()
   for i,v in ipairs(qhtEventLog) do
		if not v.replayformat then
			p("#%i: unit = player, %i entries", i, #v.player)
		else
			p("#%i: unit = %s, %i entries", i, v.unitID, #v.events)
		end
   end
end

-- Display.
local unitHealth
local quickHealth
local function displayQuickHealth(health, healthMax)
	qhtFrameQuickHealthText:SetText(f("QuickHealth %i/%i",health,healthMax))

	qhtQuickHealthBar:SetMinMaxValues(0, healthMax)
	local value = health
	local uh = unitHealth
	if currentUnit~="replay" then
		uh = UnitHealth(currentUnit)
	end

	if health==uh then
		qhtQuickHealthBarDiff:Hide()
	else
		qhtQuickHealthBarDiff:Show()
		local min, max = math.min(health, uh),math.max(health, uh)
		local width = qhtQuickHealthBar:GetWidth()
		qhtQuickHealthBarDiff:SetPoint("LEFT",qhtQuickHealthBar, "LEFT", min/healthMax * width, 0)
		qhtQuickHealthBarDiff:SetPoint("RIGHT",qhtQuickHealthBar, "LEFT", max/healthMax * width, 0)
		if uh>health then
			qhtQuickHealthBarDiff:SetVertexColor(0,1,0,0.15)
		else
			qhtQuickHealthBarDiff:SetVertexColor(0,1,0,0.75)
		end
		value = min
	end
	qhtQuickHealthBar:SetValue(value)
end

local function displayUnitHealth(health, healthMax)
	qhtFrameUnitHealthText:SetText(f("UnitHealth %i/%i",health,healthMax))
	if currentUnit == "replay" then
		displayQuickHealth(quickHealth, healthMax)
	end
end

-- Event handling.
function qhtUNIT_HEALTH(health, healthMax)
	local diff = health - unitHealth

	local green = strchar(124).."cff00ff00"
	unitHealth = health
	displayUnitHealth(health, healthMax)
end

local lastEvent
function qhtHealthUpdated(event, guid, health, healthMax)
	if currentUnit~="replay" and guid ~= UnitGUID(currentUnit) then return end
	local diff = health - quickHealth
	local queueUnit = currentUnit=="replay" and "player" or currentUnit
	local healthQueue = QuickHealth:UnitHealthQueue(queueUnit)
	local queueString
	if healthQueue then
		queueString = strjoin(',',QuickHealth:UnitHealthQueue(queueUnit))
	end
	local color = ""
	if lastEvent ~= 3 then
		if (diff >= 0 and diff <= 20) then
			color = "|cff44ff44"
			if queueString then
				queueString = "|cffffff00"..queueString..color
			end
		else
			color = "|cffff4444"
		end
	end
	p("%.3f> %sHealthUpdated %i %+i -> %i (%s)", t(), color, quickHealth, diff, health, queueString or "")
	quickHealth = health
	displayQuickHealth(health, healthMax)
end

do
   local eventFromID = {
      "|cff00ff00UNIT_HEALTH", 
      "|cffffff00UNIT_HEALTHMAX", 
      "CLEU"
   }

	function qhtHealthUpdatedDebug(event, guid, id, time,frame,hp,max)
		if event == "HealthUpdatedDebug" and guid~=UnitGUID(currentUnit) then return end
      local msg
      if id==3 then
         if max then
            msg = format("%+i/%+i", hp,max)
         else
            msg = format("%+i",hp)
         end
      else
         msg = format("%i/%i", hp,max)
      end
		lastEvent = id
		if event == "HealthUpdatedDebug" then
			p("(%5i) %.3f> %s %s", frame, time, eventFromID[id], msg)
		else
			p("#%04i (%5i) %.3f> %s %s", event, frame, time, eventFromID[id], msg)
		end
	end
end

function qhtInit(frame)
	channelIndex = GetChannelName("QuickHealthTest")

	frame:RegisterEvent('UNIT_HEALTH')
	frame:SetScript('OnEvent', function(frame, event, unitID) 
		if event=='UNIT_HEALTH' and unitID==currentUnit then
			qhtUNIT_HEALTH(UnitHealth(unitID), UnitHealthMax(unitID)) 
		end 
	end)
	local DEFAULT_BARTEXTURE = [[Interface\AddOns\QuickHealthTest\statusbar.tga]]
	do
		local widget = CreateFrame("StatusBar", "qhtQuickHealthBar", frame)
		widget:SetStatusBarTexture(DEFAULT_BARTEXTURE)
		widget:SetBackdrop{bgFile = DEFAULT_BARTEXTURE}
		widget:SetStatusBarColor(0,1,0)
		widget:SetBackdropColor(0,0.4,0)
		widget:SetPoint("CENTER", 0, -20)
		widget:SetWidth(128)
		widget:SetHeight(21)

		local t = widget:CreateTexture("qhtQuickHealthBarDiff","OVERLAY")
		t:SetTexCoordModifiesRect(true)
		t:SetPoint("TOP",widget,"TOP")
		t:SetPoint("BOTTOM",widget,"BOTTOM")
		t:SetWidth(128)
		t:SetTexture(DEFAULT_BARTEXTURE)
		t:Hide()
	end
	--[[
	do
		local widget = CreateFrame("StatusBar", "qhtUnitHealthBar", frame)
		widget:SetStatusBarTexture(DEFAULT_BARTEXTURE)
		widget:SetBackdrop{bgFile = DEFAULT_BARTEXTURE}
		widget:SetStatusBarColor(0.2,0.8,0.2)
		widget:SetBackdropColor(0,0.3,0)
		widget:SetPoint("CENTER", 0, -20-21)
		widget:SetWidth(128)
		widget:SetHeight(21)
	end
	--]]

	qhtSetCurrentUnit("player")

	QuickHealth.RegisterCallback("QuickHealthTest", "HealthUpdatedDebug", qhtHealthUpdatedDebug)
	QuickHealth.RegisterCallback("QuickHealthTest", "HealthUpdated", qhtHealthUpdated)
end

function qhtSetCurrentUnit(unitID)
	currentUnit = unitID
	local health = UnitHealth(unitID) or 100
	local healthMax = UnitHealthMax(unitID) or 100

	unitHealth = health
	quickHealth = health
	displayQuickHealth(health, healthMax)
	displayUnitHealth(health, healthMax)
end
